package org.http4k.filter

import com.natpryce.hamkrest.absent
import com.natpryce.hamkrest.and
import com.natpryce.hamkrest.assertion.assertThat
import com.natpryce.hamkrest.equalTo
import com.natpryce.hamkrest.present
import com.natpryce.hamkrest.throws
import org.http4k.core.Body
import org.http4k.core.ContentType
import org.http4k.core.ContentType.Companion.APPLICATION_JSON
import org.http4k.core.ContentType.Companion.OCTET_STREAM
import org.http4k.core.ContentType.Companion.TEXT_HTML
import org.http4k.core.Headers
import org.http4k.core.Method.DELETE
import org.http4k.core.Method.GET
import org.http4k.core.Method.OPTIONS
import org.http4k.core.Method.POST
import org.http4k.core.Request
import org.http4k.core.RequestContext
import org.http4k.core.Response
import org.http4k.core.Status
import org.http4k.core.Status.Companion.BAD_REQUEST
import org.http4k.core.Status.Companion.INTERNAL_SERVER_ERROR
import org.http4k.core.Status.Companion.I_M_A_TEAPOT
import org.http4k.core.Status.Companion.NOT_FOUND
import org.http4k.core.Status.Companion.OK
import org.http4k.core.Status.Companion.UNSUPPORTED_MEDIA_TYPE
import org.http4k.core.Uri
import org.http4k.core.cookie.cookies
import org.http4k.core.then
import org.http4k.filter.CorsPolicy.Companion.UnsafeGlobalPermissive
import org.http4k.filter.FLASH_COOKIE
import org.http4k.filter.GzipCompressionMode.Mixed
import org.http4k.filter.GzipCompressionMode.Streaming
import org.http4k.filter.SamplingDecision.Companion.DO_NOT_SAMPLE
import org.http4k.filter.SamplingDecision.Companion.SAMPLE
import org.http4k.filter.ServerFilters.ValidateRequestTracingHeaders
import org.http4k.filter.ZipkinTracesStorage.Companion.INTERNAL_THREAD_LOCAL
import org.http4k.hamkrest.hasBody
import org.http4k.hamkrest.hasContentType
import org.http4k.hamkrest.hasHeader
import org.http4k.hamkrest.hasStatus
import org.http4k.lens.Header
import org.http4k.lens.Invalid
import org.http4k.lens.LensFailure
import org.http4k.lens.Missing
import org.http4k.lens.Unsupported
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.fail
import java.io.PrintWriter
import java.io.StringWriter

class ServerFiltersTest {

    @BeforeEach
    fun before() {
        INTERNAL_THREAD_LOCAL.remove()
    }

    @Nested
    inner class ZipkinTraceFilters {
        @Test
        fun `initialises request tracing on request and sets on outgoing response when not present`() {
            var newThreadLocal: ZipkinTraces? = null
            val svc = ServerFilters.RequestTracing().then {
                newThreadLocal = ZipkinTracesStorage.THREAD_LOCAL.forCurrentThread()
                assertThat(newThreadLocal!!.traceId, present())
                assertThat(newThreadLocal!!.spanId, present())
                assertThat(newThreadLocal!!.parentSpanId, absent())
                assertThat(newThreadLocal!!.samplingDecision, equalTo(SAMPLE))

                val setOnRequest = ZipkinTraces(it)
                assertThat(setOnRequest.traceId, equalTo(newThreadLocal!!.traceId))
                assertThat(setOnRequest.spanId, equalTo(newThreadLocal!!.spanId))
                assertThat(setOnRequest.parentSpanId, absent())
                assertThat(setOnRequest.samplingDecision, equalTo(newThreadLocal!!.samplingDecision))
                Response(OK)
            }

            val received = ZipkinTraces(svc(Request(GET, "")))

            assertThat(received, equalTo(ZipkinTraces(newThreadLocal!!.traceId, newThreadLocal!!.spanId, null, SAMPLE)))
        }


        @Test
        fun `uses a new request trace every time`() {
            val traces = mutableListOf<ZipkinTraces>()

            val svc = ServerFilters.RequestTracing().then {
                traces += ZipkinTraces(it)
                Response(OK)
            }

            svc(Request(GET, ""))
            svc(Request(GET, ""))

            assertThat(traces[0], !equalTo(traces[1]))
        }

        @Test
        fun `uses existing request tracing from request and sets on outgoing response`() {
            val originalTraceId = TraceId("originalTrace")
            val originalSpanId = TraceId("originalSpan")
            val originalParentSpanId = TraceId("originalParentSpanId")
            val originalTraces = ZipkinTraces(originalTraceId, originalSpanId, originalParentSpanId, DO_NOT_SAMPLE)

            var start: Pair<Request, ZipkinTraces>? = null
            var end: Triple<Request, Response, ZipkinTraces>? = null

            val svc = ServerFilters.RequestTracing(
                { req, trace -> start = req to trace },
                { req, resp, trace -> end = Triple(req, resp, trace) }
            ).then {
                val actual = ZipkinTracesStorage.THREAD_LOCAL.forCurrentThread()
                val setOnRequest = ZipkinTraces(it)

                assertThat(actual, equalTo(originalTraces))
                assertThat(setOnRequest, equalTo(originalTraces))
                Response(OK)
            }

            val originalRequest = ZipkinTraces(originalTraces, Request(GET, ""))
            val actual = svc(originalRequest)
            assertThat(ZipkinTraces(actual), equalTo(originalTraces))

            assertThat(start!!.first, equalTo(originalRequest))
            assertThat(start!!.second, equalTo(originalTraces))

            assertThat(end!!.first, equalTo(originalRequest))
            assertThat(end!!.second, equalTo(ZipkinTraces(originalTraces, Response(OK))))
            assertThat(end!!.third, equalTo(originalTraces))
        }

        private val trace_id = "x-b3-traceid"

        private fun appExpecting(headerValue: String) = ValidateRequestTracingHeaders().then {
            assertThat(it.header(trace_id), equalTo(headerValue))
            Response(OK)
        }

        @Test
        fun `passes valid zipkin trace id on`() {
            val value = "abcdefabcdef"
            val app = appExpecting(value)

            app(Request(GET, Uri.of("/")).header(trace_id, value))
        }

        @Test
        fun `passes valid zipkin trace id on - longer transaction id`() {
            val value = "a780af1043fb429ea1cd8e9dd7ee1c70"
            val app = appExpecting(value)
            app(Request(GET, Uri.of("/")).header(trace_id, value))
        }

        val appExpectingNoHeader = ValidateRequestTracingHeaders().then {
            assertThat(it.header(trace_id), absent())
            Response(OK)
        }

        @Test
        fun `filters invalid zipkin trace ids - invalid chars`() {
            appExpectingNoHeader(Request(GET, Uri.of("/")).header(trace_id, "012345678z012345"))
        }

        @Test
        fun `filters invalid zipkin trace ids - too short`() {
            appExpectingNoHeader(Request(GET, Uri.of("/")).header(trace_id, "abcdef"))
        }

        @Test
        fun `filters invalid zipkin span ids`() {
            val header = Request(GET, Uri.of("/"))
                .header(trace_id, "a780af1043fb429ea1cd8e9dd7ee1c70") // valid
                .header("x-b3-spanid", "11111") // too short
            appExpectingNoHeader(header)
        }

        @Test
        fun `filters invalid zipkin parent span ids`() {
            val header = Request(GET, Uri.of("/"))
                .header(trace_id, "a780af1043fb429ea1cd8e9dd7ee1c70") // valid
                .header("x-b3-parentspanid", "11111") // too short
            appExpectingNoHeader(header)
        }

        @Test
        fun `filters invalid zipkin trace ids - too long`() {
            appExpectingNoHeader(
                Request(GET, Uri.of("/")).header(
                    trace_id,
                    "01234567890123456789012345678901234567890"
                )
            )
        }

        @Test
        fun `filters invalid zipkin trace ids - rejection`() {
            val app = ValidateRequestTracingHeaders(BAD_REQUEST).then { Response(OK) }
            val response =
                app(Request(GET, Uri.of("/")).header(trace_id, "01234567890123456789012345678901234567890"))
            assertThat(response.status, equalTo(BAD_REQUEST))
        }

        @Test
        fun `is compatible with our own tracing`() {
            val traced = ClientFilters.RequestTracing()
                .then(ValidateRequestTracingHeaders(BAD_REQUEST))
                .then { Response(OK) }

            assertThat(traced(Request(GET, Uri.of("/"))).status, equalTo(OK))
        }
    }

    @Nested
    inner class CorsFilters {

        @Test
        fun `GET - Cors headers are set correctly`() {
            val handler = ServerFilters.Cors(UnsafeGlobalPermissive).then { Response(I_M_A_TEAPOT) }
            val response = handler(Request(GET, "/"))

            assertThat(
                response, hasStatus(I_M_A_TEAPOT)
                    .and(hasHeader("access-control-allow-origin", "*"))
                    .and(hasHeader("access-control-allow-headers", "content-type"))
                    .and(
                        hasHeader(
                            "access-control-allow-methods",
                            "GET, POST, PUT, DELETE, OPTIONS, TRACE, PATCH, PURGE, HEAD, QUERY"
                        )
                    )
                    .and(hasHeader("access-control-allow-credentials", "true"))
                    .and(!hasHeader("access-control-expose-headers"))
                    .and(!hasHeader("access-control-max-age"))
                    .and(!hasHeader("vary"))
            )
        }

        @Test
        fun `GET - with exposed headers`() {
            val policy =
                CorsPolicy(OriginPolicy.AllowAll(), emptyList(), emptyList(), exposedHeaders = listOf("foo", "bar"))
            val handler = ServerFilters.Cors(policy).then { Response(I_M_A_TEAPOT) }
            val response = handler(Request(GET, "/"))

            assertThat(
                response, hasStatus(I_M_A_TEAPOT)
                    .and(hasHeader("access-control-expose-headers", "foo, bar"))
            )
        }

        @Test
        fun `GET - with max age`() {
            val policy = CorsPolicy(OriginPolicy.AllowAll(), emptyList(), emptyList(), maxAge = 86400)
            val handler = ServerFilters.Cors(policy).then { Response(I_M_A_TEAPOT) }
            val response = handler(Request(GET, "/"))

            assertThat(
                response, hasStatus(I_M_A_TEAPOT)
                    .and(hasHeader("access-control-max-age", "86400"))
            )
        }

        @Test
        fun `OPTIONS - requests are intercepted and returned with expected headers`() {
            val handler = ServerFilters.Cors(
                CorsPolicy(
                    OriginPolicy.AnyOf("foo", "bar"),
                    listOf("rita", "sue", "bob"),
                    listOf(DELETE, POST)
                )
            ).then { Response(INTERNAL_SERVER_ERROR) }
            val response = handler(Request(OPTIONS, "/").header("Origin", "foo"))

            assertThat(
                response, hasStatus(OK)
                    .and(hasHeader("access-control-allow-origin", "foo"))
                    .and(hasHeader("access-control-allow-headers", "rita, sue, bob"))
                    .and(hasHeader("access-control-allow-methods", "DELETE, POST"))
                    .and(!hasHeader("access-control-allow-credentials"))
                    .and(hasHeader("vary", "Origin"))
            )
        }

        @Test
        fun `OPTIONS - requests are returned with expected headers when origin does not match`() {
            val handler = ServerFilters.Cors(
                CorsPolicy(
                    OriginPolicy.AnyOf("foo", "bar"),
                    listOf("rita", "sue", "bob"),
                    listOf(DELETE, POST)
                )
            ).then { Response(INTERNAL_SERVER_ERROR) }
            val response = handler(Request(OPTIONS, "/").header("Origin", "baz"))

            assertThat(
                response, hasStatus(OK)
                    .and(!hasHeader("access-control-allow-origin"))
                    .and(!hasHeader("access-control-allow-headers"))
                    .and(!hasHeader("access-control-allow-methods"))
                    .and(!hasHeader("access-control-allow-credentials"))
                    .and(!hasHeader("vary"))
            )
        }

        @Test
        fun `OPTIONS - requests are returned with expected headers when origin is not set`() {
            val handler = ServerFilters.Cors(
                CorsPolicy(
                    OriginPolicy.AnyOf("foo", "bar"),
                    listOf("rita", "sue", "bob"),
                    listOf(DELETE, POST)
                )
            ).then { Response(INTERNAL_SERVER_ERROR) }
            val response = handler(Request(OPTIONS, "/"))

            assertThat(
                response, hasStatus(OK)
                    .and(!hasHeader("access-control-allow-origin"))
                    .and(!hasHeader("access-control-allow-headers"))
                    .and(!hasHeader("access-control-allow-methods"))
                    .and(!hasHeader("access-control-allow-credentials"))
                    .and(!hasHeader("vary"))
            )
        }

        @Test
        fun `OPTIONS - requests are returned with expected headers when AllowAll OriginPolicy is used`() {
            val handler = ServerFilters.Cors(
                CorsPolicy(
                    OriginPolicy.AllowAll(),
                    listOf("rita", "sue", "bob"),
                    listOf(DELETE, POST)
                )
            ).then { Response(INTERNAL_SERVER_ERROR) }
            val response = handler(Request(OPTIONS, "/").header("Origin", "foo"))

            assertThat(
                response, hasStatus(OK)
                    .and(hasHeader("access-control-allow-origin", "*"))
                    .and(hasHeader("access-control-allow-headers", "rita, sue, bob"))
                    .and(hasHeader("access-control-allow-methods", "DELETE, POST"))
                    .and(!hasHeader("access-control-allow-credentials"))
                    .and(!hasHeader("vary"))
            )
        }

        @Test
        fun `OPTIONS - requests are returned with expected headers when Only OriginPolicy is used`() {
            val handler = ServerFilters.Cors(
                CorsPolicy(
                    OriginPolicy.Only("foo"),
                    listOf("rita", "sue", "bob"),
                    listOf(DELETE, POST)
                )
            ).then { Response(INTERNAL_SERVER_ERROR) }
            val response = handler(Request(OPTIONS, "/").header("Origin", "foo"))

            assertThat(
                response, hasStatus(OK)
                    .and(hasHeader("access-control-allow-origin", "foo"))
                    .and(hasHeader("access-control-allow-headers", "rita, sue, bob"))
                    .and(hasHeader("access-control-allow-methods", "DELETE, POST"))
                    .and(!hasHeader("access-control-allow-credentials"))
                    .and(hasHeader("vary", "Origin"))
            )
        }

        @Test
        fun `OPTIONS - requests are returned with expected headers when AnyOf OriginPolicy is used`() {
            val handler = ServerFilters.Cors(
                CorsPolicy(
                    OriginPolicy.AnyOf(listOf("foo", "bar")),
                    listOf("rita", "sue", "bob"),
                    listOf(DELETE, POST)
                )
            ).then { Response(INTERNAL_SERVER_ERROR) }
            val response = handler(Request(OPTIONS, "/").header("Origin", "bar"))

            assertThat(
                response, hasStatus(OK)
                    .and(hasHeader("access-control-allow-origin", "bar"))
                    .and(hasHeader("access-control-allow-headers", "rita, sue, bob"))
                    .and(hasHeader("access-control-allow-methods", "DELETE, POST"))
                    .and(!hasHeader("access-control-allow-credentials"))
                    .and(hasHeader("vary", "Origin"))
            )
        }

        @Test
        fun `OPTIONS - requests are returned with expected headers when Pattern OriginPolicy is used`() {
            val handler = ServerFilters.Cors(
                CorsPolicy(
                    OriginPolicy.Pattern(Regex(".*.bar")),
                    listOf("rita", "sue", "bob"),
                    listOf(DELETE, POST)
                )
            ).then { Response(INTERNAL_SERVER_ERROR) }
            val response = handler(Request(OPTIONS, "/").header("Origin", "foo.bar"))

            assertThat(
                response, hasStatus(OK)
                    .and(hasHeader("access-control-allow-origin", "foo.bar"))
                    .and(hasHeader("access-control-allow-headers", "rita, sue, bob"))
                    .and(hasHeader("access-control-allow-methods", "DELETE, POST"))
                    .and(!hasHeader("access-control-allow-credentials"))
                    .and(hasHeader("vary", "Origin"))
            )
        }

        @Test
        fun `GET - should append 'Origin' to the existing 'Vary' list if present`() {
            val handler = ServerFilters.Cors(
                CorsPolicy(
                    OriginPolicy.Only("foo"),
                    listOf("rita", "sue", "bob"),
                    listOf(DELETE, POST)
                )
            ).then { Response(INTERNAL_SERVER_ERROR).header("Vary", "x, y") }
            val response = handler(Request(GET, "/").header("Origin", "foo"))

            assertThat(response, hasHeader("vary", "x, y, Origin"))
        }

        @Test
        fun `GET - should not append 'Origin' to the existing 'Vary' list if 'Origin' is already present`() {
            val handler = ServerFilters.Cors(
                CorsPolicy(
                    OriginPolicy.Only("foo"),
                    listOf("rita", "sue", "bob"),
                    listOf(DELETE, POST)
                )
            ).then { Response(INTERNAL_SERVER_ERROR).header("Vary", "x, Origin, y") }
            val response = handler(Request(GET, "/").header("Origin", "foo"))

            assertThat(response, hasHeader("vary", "x, Origin, y"))
        }

        @Test
        fun `GET - should not append 'Origin' to the existing 'Vary' list if the wildcard value is present`() {
            val handler = ServerFilters.Cors(
                CorsPolicy(
                    OriginPolicy.Only("foo"),
                    listOf("rita", "sue", "bob"),
                    listOf(DELETE, POST)
                )
            ).then { Response(INTERNAL_SERVER_ERROR).header("Vary", "*") }
            val response = handler(Request(GET, "/").header("Origin", "foo"))

            assertThat(response, hasHeader("vary", "*"))
        }
    }

    @Nested
    inner class CatchAllFilters {

        @Test
        fun `catch all exceptions`() {
            val e = RuntimeException("boom!")
            val handler = ServerFilters.CatchAll().then { throw e }

            val response = handler(Request(GET, "/").header("foo", "one").header("bar", "two"))

            val sw = StringWriter()
            e.printStackTrace(PrintWriter(sw))

            assertThat(response, hasStatus(INTERNAL_SERVER_ERROR).and(hasBody(sw.toString())))
        }

        @Test
        fun `catch all exceptions but let out just throwables`() {
            val e = Throwable("boom!")
            val handler = ServerFilters.CatchAll().then { throw e }

            try {
                handler(Request(GET, "/").header("foo", "one").header("bar", "two"))
                fail("Should have leaked throwable")
            } catch (t: Throwable) {
                assertThat(t, equalTo(e))
            }
        }
    }

    @Nested
    inner class CopyHeadersFilters {

        @Test
        fun `copy headers from request to response`() {
            val handler = ServerFilters.CopyHeaders("foo", "bar").then { Response(OK) }

            val response = handler(Request(GET, "/").header("foo", "one").header("bar", "two"))

            assertThat(response, hasHeader("foo", "one"))
            assertThat(response, hasHeader("bar", "two"))
        }

        @Test
        fun `copy only headers specified in filter`() {
            val handler = ServerFilters.CopyHeaders("a", "b").then { Response(OK) }

            val response = handler(Request(GET, "/").header("b", "2").header("c", "3"))

            assertThat(response.headers, equalTo(listOf("b" to "2") as Headers))
        }
    }

    @Nested
    inner class GzipFilters {
        @Test
        fun `gunzip request and gzip response`() {
            val handler = ServerFilters.GZip().then {
                assertThat(it, hasBody(equalTo<String>("hello")))
                Response(OK).body(it.body)
            }

            assertThat(
                handler(
                    Request(GET, "/").header("accept-encoding", "gzip").header("content-encoding", "gzip")
                        .body(Body("hello").gzipped().body)
                ),
                hasHeader("content-encoding", "gzip").and(hasBody(equalTo<Body>(Body("hello").gzipped().body)))
            )
        }

        @Test
        fun `handle empty messages with incorrect content-encoding`() {
            val handler = ServerFilters.GZip().then {
                assertThat(it, hasBody(equalTo<Body>(Body.EMPTY)))
                Response(OK).body(it.body)
            }

            assertThat(
                handler(
                    Request(GET, "/").header("accept-encoding", "gzip").header("content-encoding", "gzip")
                        .body(Body.EMPTY)
                ),
                hasBody(equalTo<Body>(Body.EMPTY)).and(!hasHeader("content-encoding", "gzip"))
            )
        }

        @Test
        fun `passes through non-gzipped request`() {
            val handler = ServerFilters.GZip().then {
                assertThat(it, hasBody("hello"))
                Response(OK).body("hello")
            }

            handler(Request(GET, "/").body("hello"))
        }

        @Test
        fun `gunzip request and gzip response with matching content type`() {
            val handler = ServerFilters.GZipContentTypes(setOf(ContentType.TEXT_PLAIN)).then {
                assertThat(it, hasBody(equalTo<String>("hello")))
                Response(OK).header("content-type", "text/plain").body(it.body)
            }

            assertThat(
                handler(
                    Request(GET, "/").header("accept-encoding", "gzip").header("content-encoding", "gzip")
                        .body(Body("hello").gzipped().body)
                ),
                hasHeader("content-encoding", "gzip").and(hasBody(equalTo<Body>(Body("hello").gzipped().body)))
            )
        }

        @Test
        fun `gunzip request and do not gzip response with unmatched content type`() {
            val handler = ServerFilters.GZipContentTypes(setOf(TEXT_HTML)).then {
                assertThat(it, hasBody(equalTo<String>("hello")))
                Response(OK).header("content-type", "text/plain").body(it.body)
            }

            assertThat(
                handler(
                    Request(GET, "/").header("accept-encoding", "gzip").header("content-encoding", "gzip")
                        .body(Body("hello").gzipped().body)
                ),
                !hasHeader("content-encoding", "gzip").and(hasBody(equalTo<Body>(Body("hello"))))
            )
        }

        @Test
        fun `passes through non-gzipped request despite content type`() {
            val handler = ServerFilters.GZipContentTypes(setOf(TEXT_HTML)).then {
                assertThat(it, hasBody("hello"))
                Response(OK).body("hello")
            }

            handler(Request(GET, "/").body("hello"))
        }
    }

    @Nested
    inner class GzipStreamFilters {
        @Test
        fun `gunzip request and gzip response`() {
            val handler = ServerFilters.GZip(Streaming()).then {
                assertThat(it, hasBody(equalTo<String>("hello")))
                Response(OK).body(Body("hello"))
            }

            assertThat(
                handler(
                    Request(GET, "/").header("accept-encoding", "gzip").header("content-encoding", "gzip")
                        .body(Body("hello").gzipped().body)
                ),
                hasHeader("content-encoding", "gzip").and(hasBody(equalTo<Body>(Body("hello").gzippedStream().body)))
            )
        }

        @Test
        fun `gzip stream request in mixed should return gzip stream body`() {
            val handler = ServerFilters.GZip(Mixed()).then {
                assertThat(it, hasBody(equalTo<String>("hello")))
                Response(OK).body(Body("world".byteInputStream()))
            }

            assertThat(
                handler(
                    Request(GET, "/")
                        .header("accept-encoding", "gzip")
                        .header("content-encoding", "gzip")
                        .body(Body("hello").gzipped().body)
                ),
                hasHeader("content-encoding", "gzip")
                    .and(hasBody(equalTo<Body>(Body("world").gzippedStream().body)))
            )
        }

        @Test
        fun `gzip memory request in mixed should return gzip memory body`() {
            val handler = ServerFilters.GZip(Mixed()).then {
                assertThat(it, hasBody(equalTo<String>("hello")))
                Response(OK).body(Body("world"))
            }

            assertThat(
                handler(
                    Request(GET, "/")
                        .header("accept-encoding", "gzip")
                        .header("content-encoding", "gzip")
                        .body(Body("hello").gzipped().body)
                ),
                hasHeader("content-encoding", "gzip")
                    .and(hasBody(equalTo<Body>(Body("world").gzipped().body)))
            )
        }

        @Test
        fun `handle empty messages with incorrect content-encoding`() {
            val handler = ServerFilters.GZip(Streaming()).then {
                assertThat(it, hasBody(equalTo<Body>(Body.EMPTY)))
                Response(OK).body(it.body)
            }

            assertThat(
                handler(
                    Request(GET, "/").header("accept-encoding", "gzip").header("content-encoding", "gzip")
                        .body(Body.EMPTY)
                ),
                hasBody(equalTo<Body>(Body.EMPTY)).and(!hasHeader("content-encoding", "gzip"))
            )
        }

        @Test
        fun `passes through non-gzipped request`() {
            val handler = ServerFilters.GZip(Streaming()).then {
                assertThat(it, hasBody("hello"))
                Response(OK).body("hello")
            }

            handler(Request(GET, "/").body("hello"))
        }

        @Test
        fun `gunzip request and gzip response with matching content type`() {
            val handler = ServerFilters.GZipContentTypes(setOf(ContentType.TEXT_PLAIN), Streaming()).then {
                assertThat(it, hasBody(equalTo<String>("hello")))
                Response(OK).header("content-type", "text/plain").body(Body("hello"))
            }

            assertThat(
                handler(
                    Request(GET, "/").header("accept-encoding", "gzip").header("content-encoding", "gzip")
                        .body(Body("hello").gzipped().body)
                ),
                hasHeader("content-encoding", "gzip").and(hasBody(equalTo<Body>(Body("hello").gzippedStream().body)))
            )
        }

        @Test
        fun `gunzip request and do not gzip response with unmatched content type`() {
            val handler = ServerFilters.GZipContentTypes(setOf(TEXT_HTML), Streaming()).then {
                assertThat(it, hasBody(equalTo<String>("hello")))
                Response(OK).header("content-type", "text/plain").body(it.body)
            }

            assertThat(
                handler(
                    Request(GET, "/").header("accept-encoding", "gzip").header("content-encoding", "gzip")
                        .body(Body("hello").gzipped().body)
                ),
                !hasHeader("content-encoding", "gzip").and(hasBody(equalTo<Body>(Body("hello"))))
            )
        }

        @Test
        fun `passes through non-gzipped request despite content type`() {
            val handler = ServerFilters.GZipContentTypes(setOf(TEXT_HTML), Streaming()).then {
                assertThat(it, hasBody("hello"))
                Response(OK).body("hello")
            }

            handler(Request(GET, "/").body("hello"))
        }
    }

    @Nested
    inner class CatchLensFailureFilter {
        @Test
        fun `catch lens failure - custom response`() {
            val e = LensFailure(
                Invalid(Header.required("bob").meta),
                Missing(Header.required("bill").meta),
                target = Request(GET, "")
            )
            val handler = ServerFilters.CatchLensFailure { it -> Response(OK).body(it.localizedMessage) }
                .then { throw e }

            val response = handler(Request(GET, "/"))

            assertThat(response, hasStatus(OK).and(hasBody("header 'bob' must be string, header 'bill' is required")))
        }

        @Test
        fun `catch lens failure - custom response with request`() {
            val e = LensFailure(
                Invalid(Header.required("bob").meta),
                Missing(Header.required("bill").meta),
                target = Request(GET, "")
            )
            val handler = ServerFilters.CatchLensFailure { request, lensFailure ->
                if (Header.ACCEPT(request)?.accepts(APPLICATION_JSON) == true) {
                    Response(OK).body("""{"error":"${lensFailure.localizedMessage}"}""")
                        .header("Content-Type", APPLICATION_JSON.value)
                } else {
                    Response(OK).body(lensFailure.localizedMessage)
                }
            }
                .then { throw e }

            val response = handler(Request(GET, "/").header("Accept", APPLICATION_JSON.value))

            assertThat(
                response,
                hasStatus(OK).and(hasBody("""{"error":"header 'bob' must be string, header 'bill' is required"}"""))
            )
        }

        @Test
        fun `catch lens failure - invalid`() {
            val e = LensFailure(
                Invalid(Header.required("bob").meta),
                Missing(Header.required("bill").meta),
                target = Request(GET, "")
            )
            val handler = ServerFilters.CatchLensFailure().then { throw e }

            val response = handler(Request(GET, "/"))

            assertThat(response, hasStatus(BAD_REQUEST))
            assertThat(response.status.description, equalTo("header 'bob' must be string; header 'bill' is required"))
        }

        @Test
        fun `catch lens failure - invalid from Response is rethrown`() {
            val e = LensFailure(
                Invalid(Header.required("bob").meta),
                Missing(Header.required("bill").meta),
                target = Response(OK)
            )
            val handler = ServerFilters.CatchLensFailure().then { throw e }
            assertThat({ handler(Request(GET, "/")) }, throws(equalTo(e)))
        }

        @Test
        fun `catch lens failure - invalid from RequestContext is rethrown`() {
            val e = LensFailure(
                Invalid(Header.required("bob").meta),
                Missing(Header.required("bill").meta),
                target = RequestContext()
            )
            val handler = ServerFilters.CatchLensFailure().then { throw e }
            assertThat({ handler(Request(GET, "/")) }, throws(equalTo(e)))
        }

        @Test
        fun `catch lens failure - unsupported`() {
            val e = LensFailure(Unsupported(Header.required("bob").meta), target = Request(GET, ""))
            val handler = ServerFilters.CatchLensFailure().then { throw e }

            val response = handler(Request(GET, "/"))

            assertThat(response, hasStatus(UNSUPPORTED_MEDIA_TYPE))
        }
    }

    @Test
    fun `replace response contents with static file`() {
        fun returning(status: Status) =
            ServerFilters.ReplaceResponseContentsWithStaticFile().then { Response(status).body(status.toString()) }

        assertThat(returning(NOT_FOUND)(Request(GET, "/")), hasBody("404 contents"))
        assertThat(returning(OK)(Request(GET, "/")), hasBody(OK.toString()))
    }

    @Test
    fun `set content type`() {
        val handler = ServerFilters.SetContentType(OCTET_STREAM).then { Response(OK) }

        assertThat(handler(Request(GET, "/")), hasContentType(OCTET_STREAM))
    }

    @Nested
    inner class FlashAttributesFilters {

        @Test
        fun `get flash attributes are null if not set`() {

            val handler = FlashAttributesFilter.then { request -> Response(OK).body(request.flash().orEmpty()) }

            val response = handler(Request(GET, "/"))
            assertThat(response, hasBody(""))
        }

        @Test
        fun `retrieve flash attributes if set`() {
            val handler = FlashAttributesFilter.then { _ -> Response(OK).body("abc").withFlash("Error 123") }

            val response = handler(Request(GET, "/"))
            assertThat(response.flash(), equalTo("Error 123"))
        }

        @Test
        fun `remove flash attributes after usage`() {
            val handler = FlashAttributesFilter.then { _ -> Response(OK).body("abc") }

            val request = Request(GET, "/").withFlash("input flash")
            val response = handler(request)
            assertThat(response.flash(), equalTo(""))
        }

        @Test
        fun `flash cookie setting escapes non-printable non-ASCII characters`() {
            // A string that includes characters outside the RFC6265 allowed range.
            val invalidCookieContent = "bad ‘ ’ — € \u0007 end"
            val handler = FlashAttributesFilter.then { _ -> Response(OK).withFlash(invalidCookieContent) }

            val response = handler(Request(GET, "/"))

            // Extract the encoded `flash` cookie's value of the `Set-Cookie` header.
            val cookieValue = response.cookies().firstOrNull { it.name == FLASH_COOKIE }!!.value

            // Only printable ASCII (0x21 - 0x7E) is allowed, according to the RFC6265 spec.
            val allowed = Regex("^[\\x21-\\x7E]*$")
            assertThat(allowed.matches(cookieValue), equalTo(true))
        }

        @Test
        fun `flash cookie round-trips through encoding and decoding do not change the content with unicode input`() {
            // A string that includes characters outside the RFC6265 allowed range.
            val input = "αβγ ‘ ’ — € ☃"

            val handler = FlashAttributesFilter.then { _ -> Response(OK).withFlash(input) }
            val response = handler(Request(GET, "/"))

            val output = response.flash()
            assertThat(input, equalTo(output))
        }
    }

    @Nested
    inner class ContentDispositionFilters {
        @Test
        fun `does not add content disposition if response is not 2xx`() {
            val handler = ServerFilters.ContentDispositionAttachment().then { _ -> Response(NOT_FOUND) }

            val response = handler(Request(GET, "/"))
            assertThat(response.header("Content-Disposition"), absent())
        }

        @Test
        fun `adds content disposition attachment for all extensions by default`() {
            val handler = ServerFilters.ContentDispositionAttachment().then { _ -> Response(OK).body("abc") }

            assertThat(
                handler(Request(GET, "/")).header("Content-Disposition"),
                equalTo("attachment; filename=unnamed")
            )
            assertThat(
                handler(Request(GET, "/no-extension")).header("Content-Disposition"),
                equalTo("attachment; filename=no-extension")
            )
            assertThat(
                handler(Request(GET, "/file.pdf")).header("Content-Disposition"),
                equalTo("attachment; filename=file.pdf")
            )
            assertThat(
                handler(Request(GET, "/dir/file.pdf")).header("Content-Disposition"),
                equalTo("attachment; filename=file.pdf")
            )
        }

        @Test
        fun `adds content disposition for selected types`() {
            val handler =
                ServerFilters.ContentDispositionAttachment(setOf("pdf")).then { _ -> Response(OK).body("abc") }

            assertThat(handler(Request(GET, "/")).header("Content-Disposition"), absent())
            assertThat(handler(Request(GET, "/no-extension")).header("Content-Disposition"), absent())
            assertThat(handler(Request(GET, "/not-listed-extension.png")).header("Content-Disposition"), absent())
            assertThat(
                handler(Request(GET, "/.pdf")).header("Content-Disposition"),
                equalTo("attachment; filename=.pdf")
            )
            assertThat(
                handler(Request(GET, "/file.pdf")).header("Content-Disposition"),
                equalTo("attachment; filename=file.pdf")
            )
            assertThat(
                handler(Request(GET, "/dir/file.pdf")).header("Content-Disposition"),
                equalTo("attachment; filename=file.pdf")
            )
        }
    }
}
